# Auto-fill gaps: A REAPER extension to fill gaps automatically
# Copyright (C) 2011-2012 by George Giaslas
# Contact email: ggiaslas@gmail.com
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
  
# include toolkit & python modules
from toolkit import *
import subprocess


##################
# process method #
##################

def doProcess(useDefaults, askForParameters):

  ###################################
  # 0. set default parameter values #
  ###################################
  
  # items to process: 1->only selected media items, 2->items in time selection, 3->all items in project
  itemsToProcess = 1
  
  # shift left the gap by msec for extra safety
  shiftLeftGapMsec = 5
  
  # crossfade length for filling gaps
  gapFillCrossfadeMsec = 20
  
  # crossfade type
  # 0->equal gain, 1->equal power, 2->reverse equal power, 3->steep curve, 4->reverse steep curve, 5->s-curve
  crossfadeType = 1
  
  # max gap length - don't stretch to fill more than specified gap length
  maxGapMsec = 40
  
  # min rate - don't stretch more than the specified rate
  minRate = 0.5
  
  # take care of transients - try not to stretch transients if set to true
  takeCareOfTransients = True
  
  # transient length min/max in msec
  transientLengthMinMsec = 15
  transientLengthMaxMsec = 40
  
  # crossfade length for transient split
  transientSplitCrossfadeMsec = 15
  
  # shift right the gap just before the next transient
  shiftRightGapToTransient = False
  
  # max allowed time between end of gap & next transient
  maxGapToTransientLengthMsec = 30
  
  # move the gap by the specified amount before the transient
  gapToTransientPadMsec = 6
  
  # handle whole group of items instead of each item individually
  groupHandling = True
  
  # tracks to scan for transients - leave empty to default to all selected track numbers one-based
  tracksContainingTransients = "" # separate values with pipe '|'
  
  # place a marker on a group mismatch
  warnOnGroupMismatch = True
  
  # place a marker on a transient stretch
  warnOnTransientStretched = True
  
  # place a marker on max gap/min rate limit reach
  warnOnLimitReached = True
  
  # the configuration file
  iniFileName = "autofillgaps.ini"
  

  #####################
  # 1. initialization #
  #####################
  
  # save original cursor position
  origCursor = getCursor()
  
  # begin undo block
  RPR_Undo_BeginBlock2(0)
  
  # disable auto-crossfade on split
  origXfadeOnSplit = getOverlapAndCrossfadeOnSplit()
  setOverlapAndCrossfadeOnSplit(False)
  
  # enable item info caching to limit API calls
  enableItemInfoCaching()
  
  
  ###################################
  # 2. parameter input & validation #
  ###################################
  
  # general settings
  names1 = "itemsToProcess,shiftLeftGapMsec,gapFillCrossfadeMsec,crossfadeType,maxGapMsec,minRate,groupHandling"
  types1 = "i,i,i,i,i,f,b"
  dvalues1 = "%d,%d,%d,%d,%d,%f,%d" % (itemsToProcess,shiftLeftGapMsec,gapFillCrossfadeMsec,crossfadeType,maxGapMsec,minRate,groupHandling)
  
  # transient handling settings
  names2 = "takeCareOfTransients,transientLengthMinMsec,transientLengthMaxMsec,transientSplitCrossfadeMsec,shiftRightGapToTransient,maxGapToTransientLengthMsec,gapToTransientPadMsec,tracksContainingTransients"
  types2 = "b,i,i,i,b,i,i,s"
  dvalues2 = "%d,%d,%d,%d,%d,%d,%d,%s" % (takeCareOfTransients,transientLengthMinMsec,transientLengthMaxMsec,transientSplitCrossfadeMsec,shiftRightGapToTransient,maxGapToTransientLengthMsec,gapToTransientPadMsec,tracksContainingTransients)
  
  # error handling settings
  names3 = "warnOnGroupMismatch,warnOnTransientStretched,warnOnLimitReached"
  types3 = "b,b,b"
  dvalues3 = "%d,%d,%d" % (warnOnGroupMismatch,warnOnTransientStretched,warnOnLimitReached)
  
  # attempt to read parameters from ini file
  allNames = names1+','+names2+','+names3
  allTypes = types1+','+types2+','+types3
  allDvalues = dvalues1+','+dvalues2+','+dvalues3
  ret = readParameters(iniFileName, "settings", allNames, allTypes, allDvalues)
  if ret:
    # assign values to parameters
    if not useDefaults:
      (itemsToProcess,shiftLeftGapMsec,gapFillCrossfadeMsec,crossfadeType,maxGapMsec,minRate,groupHandling,takeCareOfTransients,transientLengthMinMsec,transientLengthMaxMsec,transientSplitCrossfadeMsec,shiftRightGapToTransient,maxGapToTransientLengthMsec,gapToTransientPadMsec,tracksContainingTransients,warnOnGroupMismatch,warnOnTransientStretched,warnOnLimitReached) = ret
  else:
    # create file with default values
    writeParameters(iniFileName, "settings", allNames, allDvalues)
    # show alert on first execution
    alert("Auto-fill gaps - Copyright (C) 2011-2012 by George Giaslas", "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND; for more details see 'LICENSE.txt'\nFor usage instructions see 'README.txt'.")

  # create/overwrite 'defaults' section in the ini file
  writeParameters(iniFileName, "defaults", allNames, allDvalues)
  
  
  # ask the user to modify the parameters
  needToUpdateIniFile = False
  if askForParameters:
    # for windows platforms launch the external UI
    uiAppPath = RPR_GetResourcePath() + '\\' + "Scripts\\AutoFillGapsUI.exe"
    if isWindowsPlatform() and RPR_file_exists(uiAppPath):
      arg1 = RPR_GetResourcePath() + '\\' + iniFileName
      arg2 = convertToBase64("|".join(getTrackNames()))
      return_code = subprocess.call([uiAppPath, arg1, arg2], shell=False)
      if return_code != 0:
        return False
      
      # read the entered parameters now
      ret = readParameters(iniFileName, "settings", allNames, allTypes, allDvalues)
      (itemsToProcess,shiftLeftGapMsec,gapFillCrossfadeMsec,crossfadeType,maxGapMsec,minRate,groupHandling,takeCareOfTransients,transientLengthMinMsec,transientLengthMaxMsec,transientSplitCrossfadeMsec,shiftRightGapToTransient,maxGapToTransientLengthMsec,gapToTransientPadMsec,tracksContainingTransients,warnOnGroupMismatch,warnOnTransientStretched,warnOnLimitReached) = ret
    
    # for other platforms launch the internal UI
    else:
      # first dialog
      values1 = "%d,%d,%d,%d,%d,%f,%d" % (itemsToProcess,shiftLeftGapMsec,gapFillCrossfadeMsec,crossfadeType,maxGapMsec,minRate,groupHandling)
      ret = enterParameters("1 - General settings", names1, types1, values1)
      if not ret:
        return False
      (itemsToProcess,shiftLeftGapMsec,gapFillCrossfadeMsec,crossfadeType,maxGapMsec,minRate,groupHandling) = ret
  
      # second dialog
      values2 = "%d,%d,%d,%d,%d,%d,%d,%s" % (takeCareOfTransients,transientLengthMinMsec,transientLengthMaxMsec,transientSplitCrossfadeMsec,shiftRightGapToTransient,maxGapToTransientLengthMsec,gapToTransientPadMsec,tracksContainingTransients)
      ret = enterParameters("2 - Transient handling settings", names2, types2, values2)
      if not ret:
        return False
      (takeCareOfTransients,transientLengthMinMsec,transientLengthMaxMsec,transientSplitCrossfadeMsec,shiftRightGapToTransient,maxGapToTransientLengthMsec,gapToTransientPadMsec,tracksContainingTransients) = ret
      needToUpdateIniFile = True

  
  # validate items to process. valid values: 1, 2, 3
  if not (itemsToProcess==1 or itemsToProcess==2 or itemsToProcess==3):
    error("Invalid 'itemsToProcess' option: %d" % itemsToProcess)
    return False
    
  # validate shiftLeftGapMsec. valid values: 0-100msec
  if not (shiftLeftGapMsec >= 0 and shiftLeftGapMsec <= 100):
    error("Invalid 'shiftLeftGapMsec' option: %d" % shiftLeftGapMsec)
    return False
    
  # validate gapFillCrossfadeMsec. valid values: 0-100msec
  if not (gapFillCrossfadeMsec >= 0 and gapFillCrossfadeMsec <= 100):
    error("Invalid 'gapFillCrossfadeMsec' option: %d" % gapFillCrossfadeMsec)
    return False

  # validate crossfadeType. valid values: 0-5
  if not (crossfadeType >= 0 and crossfadeType <= 5):
    error("Invalid 'crossfadeType' option: %d" % crossfadeType)
    return False
    
  # validate maxGapMsec. valid values: 1-1000msec
  if not (maxGapMsec >= 1 and maxGapMsec <= 1000):
    error("Invalid 'maxGapMsec' option: %d" % maxGapMsec)
    return False

  # validate minRate. valid values: 0.0-1.0
  if not (minRate >= 0.0 and minRate <= 1.0):
    error("Invalid 'minRate' option: %f" % minRate)
    return False
    
  # validate transientLengthMinMsec/transientLengthMaxMsec. valid values: 1-1000msec
  if not (transientLengthMinMsec >= 1 and transientLengthMinMsec <= 1000):
    error("Invalid 'transientLengthMinMsec' option: %d" % transientLengthMinMsec)
    return False
  if not (transientLengthMaxMsec >= 1 and transientLengthMaxMsec <= 1000):
    error("Invalid 'transientLengthMaxMsec' option: %d" % transientLengthMaxMsec)
    return False
  if isAGreaterThanB(transientLengthMinMsec, transientLengthMaxMsec):
    error("Invalid 'transientLengthMaxMsec' option: %d. Should be greater than 'transientLengthMinMsec'" % transientLengthMaxMsec)
    return False

  # validate transientSplitCrossfadeMsec. valid values: 0-100msec
  if not (transientSplitCrossfadeMsec >= 0 and transientSplitCrossfadeMsec <= 100):
    error("Invalid 'transientSplitCrossfadeMsec' option: %d" % transientSplitCrossfadeMsec)
    return False
    
  # validate maxGapToTransientLengthMsec. valid values: 0-100msec
  if not (maxGapToTransientLengthMsec >= 0 and maxGapToTransientLengthMsec <= 100):
    error("Invalid 'maxGapToTransientLengthMsec' option: %d" % maxGapToTransientLengthMsec)
    return False

  # validate gapToTransientPadMsec. valid values: 0-100msec
  if not (gapToTransientPadMsec >= 0 and gapToTransientPadMsec <= 100):
    error("Invalid 'gapToTransientPadMsec' option: %d" % gapToTransientPadMsec)
    return False

  
  # update the ini file with the new values
  if needToUpdateIniFile:
    values1 = "%d,%d,%d,%d,%d,%f,%d" % (itemsToProcess,shiftLeftGapMsec,gapFillCrossfadeMsec,crossfadeType,maxGapMsec,minRate,groupHandling)
    values2 = "%d,%d,%d,%d,%d,%d,%d,%s" % (takeCareOfTransients,transientLengthMinMsec,transientLengthMaxMsec,transientSplitCrossfadeMsec,shiftRightGapToTransient,maxGapToTransientLengthMsec,gapToTransientPadMsec,tracksContainingTransients)
    values3 = "%d,%d,%d" % (warnOnGroupMismatch,warnOnTransientStretched,warnOnLimitReached)
    allNames = names1+','+names2+','+names3
    allValues = values1+','+values2+','+values3
    writeParameters(iniFileName, "settings", allNames, allValues)
  
  
  ##################
  # 3. preparation #
  ##################
  
  # prepare list with media items to process
  itemsToProcessList = []
  
  # select items in time selection if specified
  if itemsToProcess==2:
    unselectAll()
    selectItemsInTimeSelection()
  # select all items in project if specified
  elif itemsToProcess==3:
    unselectAll()
    selectAll()
  
  # now populate the items to process list from the selected items
  selectedItems = RPR_CountSelectedMediaItems(0)
  selectedTracks = []
  for i in range(0, selectedItems):
    item = RPR_GetSelectedMediaItem(0, i)
    itemsToProcessList.append(item)
    track = getTrack(item)
    if not track in selectedTracks:
      selectedTracks.append(track)
  
  # check if there are any items to process
  if len(itemsToProcessList) == 0:
    error("no items to process!")
    return
    
  # update tracksContainingTransients option if empty
  if tracksContainingTransients == "":
    for i in range(0, len(selectedTracks)):
      tracksContainingTransients += str(int(getTrackNumber(selectedTracks[i])))
      if i != len(selectedTracks)-1:
        tracksContainingTransients += "|"
  
  # create track list for tracksContainingTransients
  # adding only the ones that are selected
  tracksSplitted = tracksContainingTransients.split('|')
  tracksContainingTransientsList = []
  for i in range(0, len(tracksSplitted)):
    track = getTrackFromNumber(tracksSplitted[i])
    if track and track in selectedTracks:
      tracksContainingTransientsList.append(track)
  
  
  # unselect all items
  unselectAll()
  
  # prepare 2d list with group of items to process
  groupsToProcessList = []
  
  # group items to process based on criteria:
  # same group id, same start, same length
  groupCnt = 0
  while len(itemsToProcessList) > 0:
    # create a new group containing initially the first item
    groupsToProcessList.append([])
    groupsToProcessList[groupCnt].append(itemsToProcessList[0])
    
    # now iterate through rest of items and find items
    # in different tracks with the same group id & same start/length
    if groupHandling and len(itemsToProcessList) > 1:
      i = 1
      while i < len(itemsToProcessList):
        if areGrouped(itemsToProcessList[0], itemsToProcessList[i]):
          # add item in group and remove it from the list
          groupsToProcessList[groupCnt].append(itemsToProcessList[i])
          itemsToProcessList.pop(i)
          i = i - 1
        i = i + 1
    
    # now remove processed item from list
    itemsToProcessList.pop(0)
    
    # and increase group counter for the next iteration
    groupCnt = groupCnt + 1
  
    
  ################
  # 4. main loop #
  ################
  
  # find the gaps to be filled
  for g in range(0, len(groupsToProcessList)-1):
    # get the first item of current & next group
    thisGroup = groupsToProcessList[g]
    nextGroup = groupsToProcessList[g+1]
    thisItem = thisGroup[0]
    nextItem = nextGroup[0]
  
    # first check if current item & its next belong to the same track
    # and ignore item if it's the last in track
    if getTrack(thisItem) != getTrack(nextItem):
      continue
  		
    # check if there is a gap between the items
    thisItemStartsAt = getHeadPosition(thisItem)
    thisItemEndsAt = getTailPosition(thisItem)
    nextItemsStartsAt = getHeadPosition(nextItem)
    if isAGreaterThanB(nextItemsStartsAt, thisItemEndsAt):
    
      # first validate groups and put a marker if there is an error
      if not validateGroups(thisGroup, nextGroup):
        if warnOnGroupMismatch:
          createMarkerAt(thisItemEndsAt, "group mismatch")
        continue
    
  ###############################
  # 5. shifting gap to the left #
  ###############################
  
      # shift left the gap if specified
      # and set next item's snap offset to its original start position
      # if group handling was on, these will performed on the whole group
      if shiftLeftGapMsec > 0:
        for i in range(0, len(thisGroup)):
          cropTail(thisGroup[i], shiftLeftGapMsec)
          extendHead(nextGroup[i], shiftLeftGapMsec)
          #setSnapOffset(nextGroup[i], shiftLeftGapMsec)
        thisItemEndsAt = getTailPosition(thisItem)
        nextItemsStartsAt = getHeadPosition(nextItem)
  
      # compute gap to be filled limited to maxGap
      realGap = nextItemsStartsAt - thisItemEndsAt
      gapToFill = min(realGap, maxGapMsec/1000.0)
      
  
  ######################################################
  # 6. shifting gap to the right before next transient #
  ######################################################
  
      # if shift right gap to next transient specified
      # find the next transient falling inside the limit
      # and shift gap before the transient
      # allow extra space before the transient: padding specified + crossfade length
      # if group handling was on, these will performed on the whole group
      if shiftRightGapToTransient:
        if groupHandling:
          transientPos = firstTransientInGroup(filterItemsByTrack(nextGroup, tracksContainingTransientsList), maxGapToTransientLengthMsec)
        else:
          transientPos = firstTransientInItem(nextItem, maxGapToTransientLengthMsec)
  
        if transientPos != -1:
          shiftRightGapMsec = (transientPos - nextItemsStartsAt)*1000.0 - gapToTransientPadMsec
          for i in range(0, len(thisGroup)):
            extendTail(thisGroup[i], shiftRightGapMsec)
            cropHead(nextGroup[i], shiftRightGapMsec)
          thisItemEndsAt = getTailPosition(thisItem)
          nextItemsStartsAt = getHeadPosition(nextItem)
  
  
  #########################
  # 7. transient handling #
  #########################
  
      # will hold the original position of transients stretched in this iteration
      stretchedTransients = []

      # if we should take care of transients and gap is big enough
      # check if the current item contains a transient
      # and if so split after defined transient length
      # so that the right-hand split item is long enough to fill the gap by stretching
      # if group handling was on, these will performed on the whole group
      if takeCareOfTransients and areEqual(realGap, gapToFill):
        # start from the very last transient
        skipLastMsec = 0
        gapFilled = False
        
        # loop backwards from last transient of item/group towards first transient
        # until we find a suitable one or none at all
        while True:
        if groupHandling:
            transientPos = lastTransientInGroup(filterItemsByTrack(thisGroup, tracksContainingTransientsList), skipLastMsec)
        else:
            transientPos = lastTransientInItem(thisItem, skipLastMsec)
  
        if transientPos != -1:
          # start with max transient length
          transientLength = transientLengthMaxMsec
          
          # loop backwards decreasing transient length by 1msec in each iteration
          while True:
            # compute rate needed for the right-hand split item to fill the gap
            # !! suppose old rate is 1.0 !!
            # TODO: maybe fix this assumption
            curLen = thisItemEndsAt - transientPos - (transientLength/1000.0)
            computedRate = curLen / (curLen + gapToFill)
            if computedRate > minRate and ((curLen + gapToFill) > (transientSplitCrossfadeMsec + gapFillCrossfadeMsec)/1000.0):
              # split item with crossfade & update 'this'
              thisGroup = splitGroupWithCrossfadeOnRight(thisGroup, transientPos + (transientLength/1000.0), transientSplitCrossfadeMsec, crossfadeType)
              thisItem = thisGroup[0]
              thisItemStartsAt = getHeadPosition(thisItem)
              thisItemEndsAt = getTailPosition(thisItem)
              
              # break out of while loop
                gapFilled = True
              break
              
            # decrease transient length
            # and break if reached min limit
            transientLength -= 1.0
            if transientLength < transientLengthMinMsec:
                # put markers later after stretch factor is computed
              if warnOnTransientStretched:
                  stretchedTransients.append(transientPos)
              break
          
          # if found a suitable transient and gap was filled 
          # or there are no more transients in this item/group then break the loop
          # else continue loop to the previous transient in the item/group
          if transientPos == -1 or gapFilled:
            break
          else:
            skipLastMsec = 10.0 + math.ceil((thisItemEndsAt - transientPos) * 1000.0)
  
  
  ##################
  # 8. gap filling #
  ##################
  
      # compute rate that would be needed to fill
      # the real gap without any limitations
      curLen = getLength(thisItem)
      optimalRate = curLen / (curLen + realGap)

      # compute rate to fill the resulting gap limited to minRate
      # !! suppose old rate is 1.0 !!
      # TODO: maybe fix this assumption
      computedRate = curLen / (curLen + gapToFill)
      finalRate = max(computedRate, minRate)
      
      # set the items' new playrate
      # it also extends the length of the item
      for i in range(0, len(thisGroup)):
        setPlayrate(thisGroup[i], finalRate)
      thisItemEndsAt = getTailPosition(thisItem)
      
      # if the resulting rate IS NOT the optimal one
      # i.e. max gap/min rate limits where reached
      # fill the remaining gap by extending the head of the next item
      # and put a marker at that position
      if not areEqual(finalRate, optimalRate):
        extendNextItemsHeadByMsec = (nextItemsStartsAt - thisItemEndsAt) * 1000
        nextItemStartsAt = getHeadPosition(nextItem)
        for i in range(0, len(nextGroup)):
          extendHead(nextGroup[i], extendNextItemsHeadByMsec)
        if warnOnLimitReached:
          createMarkerAt(nextItemStartsAt, "gap/rate limit reached")
      
      # set crossfades
      if gapFillCrossfadeMsec > 0:
        for i in range(0, len(thisGroup)):
          extendHead(nextGroup[i], gapFillCrossfadeMsec)
          setFadeOut(thisGroup[i], gapFillCrossfadeMsec, crossfadeType)
          setFadeIn(nextGroup[i], gapFillCrossfadeMsec, crossfadeType)
  
      # place transient stretching markers
      # on transient positions after stretching
      for transPos in stretchedTransients:
        newTransPos = thisItemStartsAt + (transPos - thisItemStartsAt) / finalRate
        createMarkerAt(newTransPos, "transient stretched")

      
  ###################
  # 9. post-process #
  ###################
  
  # restore cursor to original position
  setCursor(origCursor)
  
  # restore crossfade on spit
  setOverlapAndCrossfadeOnSplit(origXfadeOnSplit)
  
  # unselect all items
  unselectAll()
  
  # end undo block
  RPR_Undo_EndBlock2(0, "Auto-fill gaps", 0)

  return True


########
# main #
########
if __name__ == "__main__":
  doProcess(False, True)


############
# COMPLETE #
############
